package me.edzh.spring.bootstrap.processor;

import lombok.extern.log4j.Log4j;
import me.edzh.spring.bootstrap.model.HashMapWrapper;
import me.edzh.spring.bootstrap.utils.ObjectMapperFactory;
import me.edzh.spring.bootstrap.utils.ResultCode;
import me.edzh.spring.bootstrap.utils.exception.ServiceException;
import me.edzh.spring.bootstrap.utils.security.RSAUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;

import static com.yhb.utils.Constants.RSA_SEGMENT;
import static com.yhb.utils.Constants.RSA_SEGMENT_DECRYPT;

@Log4j
@Service
public class SignAndEncryptProcessor {


    @Autowired(required = false)
    private SignConfig signConfig;

    @Autowired(required = false)
    private EncryptConfig encryptConfig;

    public <T> T checkSignAndConvertBody(String sign, byte[] body, boolean needRSA, Class<T> clazz) throws IOException {
        if (signConfig == null) {
            log.warn("should implement me.edzh.spring.bootstrap.processors.SignAndEncryptProcessors.SignConfig for enable signing features");
            throw new ServiceException(ResultCode.ERROR_INTERNAL_DATA_CODE, "服务器配置错误");
        }


        byte[] content = body;
        if (needRSA) {
            if (encryptConfig == null) {
                log.warn("should implement me.edzh.spring.bootstrap.processors.SignAndEncryptProcessors.RSAConfig for enable signing features");
                throw new ServiceException(ResultCode.ERROR_INTERNAL_DATA_CODE, "服务器配置错误");
            }

            try {
                content = RSAUtils.decrypt(content, encryptConfig.privateKey(), RSA_SEGMENT_DECRYPT);
            } catch (Throwable t) {
                throw new ServiceException(ResultCode.ERROR_DECRYPT_ERR, "解密失败", t);
            }
        }


        @SuppressWarnings("unchecked") Map<String, Object> param = ObjectMapperFactory.getObjectMapper().readValue(content, HashMap.class);

        if (checkSign(param, sign, this.signConfig)) {
            param.remove("timeStamp"); // 校验完后时间戳移除, 对业务层不暴露这类参数

//            log.info(clazz.toString());
            if (clazz.equals(HashMapWrapper.class)) {
                return clazz.cast(new HashMapWrapper(param));
            }
            return ObjectMapperFactory.getObjectMapper().convertValue(param, clazz);
        }

        throw new ServiceException(ResultCode.ERROR_SIGN_CHECK_ERR, "签名校验失败");
    }

    private boolean checkSign(Map<String, Object> params, String sign, SignConfig signer) {
        try {
            long timeStamp = Long.parseLong(params.get("timeStamp").toString());
            log.info("client timeStamp:" + timeStamp);
            if (Math.abs(System.currentTimeMillis() - timeStamp) > signer.timeStampInvalidThreshold()) { // 检查签名时间是否合法
                throw new RuntimeException();
            }
        } catch (Exception e) {
            throw new ServiceException(ResultCode.ERROR_SIGN_STAMP_OLD_ERR, "时间戳错误, 请校准客户端系统时间", e);
        }


        String signStr = getParamSignString(params);
        log.info("signStr=" + signStr);
        String serverSigned = signer.sign(signStr).toUpperCase();
        log.info("ServerSign:" + serverSigned);
        return sign.equals(serverSigned);
    }


    public interface SignConfig {
        String sign(String signStr);

        long timeStampInvalidThreshold();
    }

    public interface EncryptConfig {
        PrivateKey privateKey();
    }

    public static String getParamSignString(Map<String, Object> param) {
        TreeMap<String, Object> sortedMap = new TreeMap<>(param);
        StringBuilder sb = new StringBuilder();

        for (Map.Entry<String, Object> entry : sortedMap.entrySet()) {
//            log.info("key:" + entry.getKey() + "  value:" + entry.getValue().toString());
            sb.append(entry.getKey()).append("=").append(Optional.ofNullable(entry.getValue()).orElse("null").toString()).append("&");
        }

        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }
}
